# frozen_string_literal: true

require 'spec_helper'

RSpec.describe API::VulnerabilityIssueLinks, feature_category: :vulnerability_management do
  include AccessMatchersForRequest

  before do
    stub_licensed_features(security_dashboard: true)
  end

  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }

  describe 'GET /vulnerabilities/:id/issue_links' do
    let_it_be(:vulnerability) { create(:vulnerability, :with_issue_links, project: project) }
    let_it_be(:vulnerability_id) { vulnerability.id }

    let(:vulnerability_issue_links_path) { "/vulnerabilities/#{vulnerability_id}/issue_links" }

    subject(:get_issue_links) { get api(vulnerability_issue_links_path, user) }

    context 'with an authorized user with proper permissions' do
      before do
        project.add_developer(user)
      end

      shared_examples "responds with list of only visible issue links" do
        it 'gets the list of visible issue links', :aggregate_failures do
          get_issue_links

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to match_response_schema('public_api/v4/vulnerability_related_issues', dir: 'ee')
          expect(json_response.map { |link| link['id'] }).to match_array(vulnerability.related_issues.map(&:id))
          expect(json_response.map { |link| link['vulnerability_link_id'] }).to(
            match_array(vulnerability.issue_links.map(&:id)))
          expect(json_response.map { |link| link['vulnerability_link_type'] }).to all eq 'related'
        end
      end

      context 'when linked issue is not confidential and available for the user' do
        include_examples 'responds with list of only visible issue links'
      end

      context 'when there is an additional confidential issue linked' do
        let_it_be(:public_project) { create(:project, :public) }
        let_it_be(:confidential_issue) { create(:issue, :confidential, project: public_project) }
        let_it_be(:confidential_issue_link) { create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: confidential_issue) }

        include_examples 'responds with list of only visible issue links'

        it 'does not return confidential issue in the response', :aggregate_failures do
          get_issue_links

          expect(json_response.map { |link| link['id'] }).not_to include(confidential_issue.id)
          expect(json_response.map { |link| link['vulnerability_link_id'] }).not_to include(confidential_issue_link.id)
        end
      end

      context 'when link is created to issue in the inaccessible project' do
        let_it_be(:private_project) { create(:project, :private) }
        let_it_be(:private_issue) { create(:issue, :confidential, project: private_project) }
        let_it_be(:private_issue_link) { create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: private_issue) }

        include_examples 'responds with list of only visible issue links'

        it 'does not return issue from inaccessible project', :aggregate_failures do
          get_issue_links

          expect(json_response.map { |link| link['id'] }).not_to include(private_issue.id)
          expect(json_response.map { |link| link['vulnerability_link_id'] }).not_to include(private_issue_link.id)
        end
      end

      it_behaves_like 'responds with "not found" for an unknown vulnerability ID'

      it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
    end

    describe 'permissions', :enable_admin_mode do
      it { expect { get_issue_links }.to be_allowed_for(:admin) }
      it { expect { get_issue_links }.to be_allowed_for(:owner).of(project) }
      it { expect { get_issue_links }.to be_allowed_for(:maintainer).of(project) }
      it { expect { get_issue_links }.to be_allowed_for(:developer).of(project) }
      it { expect { get_issue_links }.to be_allowed_for(:auditor) }

      it { expect { get_issue_links }.to be_denied_for(:reporter).of(project) }
      it { expect { get_issue_links }.to be_denied_for(:guest).of(project) }
      it { expect { get_issue_links }.to be_denied_for(:anonymous) }
    end
  end

  describe 'POST /vulnerabilities/:id/issue_links' do
    let_it_be(:issue) { create(:issue, project: project) }
    let_it_be(:vulnerability) { create(:vulnerability, project: project) }

    let(:vulnerability_id) { vulnerability.id }
    let(:target_issue_iid) { issue.iid }
    let(:params) { { target_issue_iid: target_issue_iid } }

    subject(:create_issue_link) do
      post api("/vulnerabilities/#{vulnerability_id}/issue_links", user), params: params
    end

    context 'with an authorized user with proper permissions' do
      before do
        project.add_developer(user)
      end

      context 'with valid params' do
        it 'creates a new vulnerability-issue link', :aggregate_failures do
          create_issue_link

          expect(response).to have_gitlab_http_status(:created)
          expect(response).to match_response_schema('public_api/v4/vulnerability_issue_link', dir: 'ee')
          expect(json_response['id']).to eq Vulnerabilities::IssueLink.last.id
          expect(json_response['issue']['id']).to eq issue.id
          expect(json_response['vulnerability']['id']).to eq vulnerability.id
        end
      end

      context 'when issue is from different project' do
        let_it_be(:other_issue) { create(:issue) }

        let(:target_project_id) { other_issue.project_id }

        let(:params) { { target_issue_iid: other_issue.iid, target_project_id: target_project_id } }

        context 'when target_project_id is invalid' do
          let(:target_project_id) { 0 }

          it 'responds with "not found" and specific error message' do
            create_issue_link

            expect(response).to have_gitlab_http_status(:not_found)
          end
        end

        context 'when user does not have access to the project' do
          it 'responds with "not found" and specific error message' do
            create_issue_link

            expect(response).to have_gitlab_http_status(:not_found)
          end
        end

        context 'when user is authorized with proper permissions to the project' do
          before do
            other_issue.project.add_developer(user)
          end

          it 'creates a new vulnerability-issue link', :aggregate_failures do
            create_issue_link

            expect(response).to have_gitlab_http_status(:created)
            expect(response).to match_response_schema('public_api/v4/vulnerability_issue_link', dir: 'ee')
            expect(json_response['id']).to eq Vulnerabilities::IssueLink.last.id
            expect(json_response['issue']['id']).to eq other_issue.id
            expect(json_response['vulnerability']['id']).to eq vulnerability.id
          end
        end
      end

      context 'with unknown issue ID' do
        let(:target_issue_iid) { 0 }

        it 'responds with "not found" and specific error message' do
          create_issue_link

          expect(response).to have_gitlab_http_status(:not_found)
        end
      end

      context 'when a link between these issue and vulnerability already exists' do
        before do
          create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: issue)
        end

        it 'responds with "conflict" status code and specific error message', :aggregate_failures do
          create_issue_link

          expect(response).to have_gitlab_http_status(:unprocessable_entity)
          expect(json_response['message']).to eq 'Issue has already been linked to another vulnerability'
        end
      end

      context 'when a "created" link for a vulnerability already exists' do
        before do
          create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: create(:issue), link_type: 'created')
        end

        let(:params) { super().merge(link_type: 'created') }

        it 'responds with "conflict" status code and specific error message', :aggregate_failures do
          create_issue_link

          expect(response).to have_gitlab_http_status(:unprocessable_entity)
          expect(json_response['message']).to eq 'Vulnerability already has a "created" issue link'
        end
      end

      context 'when trying to relate a confidential issue of the same project' do
        let(:issue) { create(:issue, :confidential, project: project) }

        it 'creates a new vulnerability-issue link' do
          create_issue_link

          expect(response).to have_gitlab_http_status(:created)
        end
      end

      it_behaves_like 'responds with "not found" for an unknown vulnerability ID'

      it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
    end

    describe 'permissions', :enable_admin_mode do
      it { expect { create_issue_link }.to be_allowed_for(:admin) }
      it { expect { create_issue_link }.to be_allowed_for(:owner).of(project) }
      it { expect { create_issue_link }.to be_allowed_for(:maintainer).of(project) }
      it { expect { create_issue_link }.to be_allowed_for(:developer).of(project) }

      it { expect { create_issue_link }.to be_denied_for(:auditor) }
      it { expect { create_issue_link }.to be_denied_for(:reporter).of(project) }
      it { expect { create_issue_link }.to be_denied_for(:guest).of(project) }
      it { expect { create_issue_link }.to be_denied_for(:anonymous) }
    end
  end

  describe 'DELETE /vulnerabilities/:id/issue_links/:issue_link_id' do
    let_it_be(:vulnerability_issue_link) { create(:vulnerabilities_issue_link, project: project) }

    let(:vulnerability_id) { vulnerability_issue_link.vulnerability.id }
    let(:issue_link_id) { vulnerability_issue_link.id }

    subject(:delete_issue_link) do
      delete api("/vulnerabilities/#{vulnerability_id}/issue_links/#{issue_link_id}", user)
    end

    context 'with an authorized user with proper permissions' do
      before do
        project.add_developer(user)
      end

      context 'with valid params' do
        it 'deletes the specified vulnerability-issue link', :aggregate_failures do
          delete_issue_link

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to match_response_schema('public_api/v4/vulnerability_issue_link', dir: 'ee')
          expect(json_response['id']).to eq issue_link_id
          expect(json_response['issue']['id']).to eq vulnerability_issue_link.issue.id
          expect(json_response['vulnerability']['id']).to eq vulnerability_id
        end

        context 'with link to an issue that the current user cannot see' do
          let_it_be(:vulnerability) { create(:vulnerability, project: project) }
          let_it_be(:private_project) { create(:project, :private) }
          let_it_be(:issue) { create(:issue, project: private_project) }
          let_it_be(:vulnerability_issue_link) { create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: issue) }

          it 'deletes the link without disclosing the linked issue', :aggregate_failures do
            delete_issue_link

            expect(response).to have_gitlab_http_status(:ok)
            expect(response).to match_response_schema('public_api/v4/vulnerability_issue_link', dir: 'ee')
            expect(json_response['id']).to eq issue_link_id
            expect(json_response['issue']).to be_nil
            expect(json_response['vulnerability']['id']).to eq vulnerability_id
          end
        end
      end

      context 'with unknown issue link ID' do
        let(:issue_link_id) { 0 }

        it 'responds with "not found" and specific error message' do
          delete_issue_link

          expect(response).to have_gitlab_http_status(:not_found)
        end
      end

      it_behaves_like 'responds with "not found" for an unknown vulnerability ID'
      it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features'
    end

    describe 'permissions', :enable_admin_mode do
      it { expect { delete_issue_link }.to be_allowed_for(:admin) }
      it { expect { delete_issue_link }.to be_allowed_for(:owner).of(project) }
      it { expect { delete_issue_link }.to be_allowed_for(:maintainer).of(project) }
      it { expect { delete_issue_link }.to be_allowed_for(:developer).of(project) }

      it { expect { delete_issue_link }.to be_denied_for(:auditor) }
      it { expect { delete_issue_link }.to be_denied_for(:reporter).of(project) }
      it { expect { delete_issue_link }.to be_denied_for(:guest).of(project) }
      it { expect { delete_issue_link }.to be_denied_for(:anonymous) }
    end
  end
end
